package com.trg.flex
{
	import flash.display.DisplayObject;
	import flash.display.Sprite;
	import flash.utils.describeType;
	
	import mx.collections.ArrayCollection;
	import mx.collections.CursorBookmark;
	import mx.collections.ICollectionView;
	import mx.collections.IList;
	import mx.collections.IViewCursor;
	import mx.controls.Alert;
	import mx.core.Container;
	import mx.events.CloseEvent;
	
	/**
	 * This class houses a lot of handy general utility functions.
	 */
	public class FlexUtils
	{
		/**
		 * Return the value of the nested property of the given object. (See Nested Property.)
		 */
		public static function getNestedProperty(object:Object, property:String):Object {
			return getChainedProperty(object, getChain(property));
		}
		
		/**
		 * Return the value of the chained property of the given object. (See Nested Property.)
		 */
		public static function getChainedProperty(object:Object, chain:Array):Object {
			if (chain == null) return object;
			for (var i:int = 0; i < chain.length; i++) {
				if (object == null) return null;
				object = object[chain[i]];
			}
			return object;
		}
		
		/**
		 * Get the chain array from the nested property string.
		 */
		public static function getChain(property:String):Array {
			if (property == null) return [];
			return property.split(".");
		}
		
		/**
		 * Set the value of the chained property of the given object. If build is true,
		 * properties along the nested property chain may be set to new instances of
		 * their specified type. (see setNestedProperty.)
		 */
		public static function setChainedProperty(object:Object, chain:Array, value:Object, build:Boolean=false):void {
			if (object == null || chain == null || chain.length == 0) return;
			for (var i:int = 0; i < chain.length - 1; i++) {
				var prop:Object = chain[i];
				if (object[prop] == null) {
					if (!build) return;
					var propType:Class = MetaDataUtils.getPropertyType(object, prop as String);
					if (propType == null) propType = Object;
					object[prop] = new propType();
				}
				object = object[prop];
			}
			object[chain[chain.length - 1]] = value;
		}
		
		/**
		 * <p>Set the value of the nested property of the given object. If build is
		 * true, properties along the nested property chain may be set to new instances
		 * of their specified type. Otherwise, the property value will not be set if 
		 * on of the objects along the chain is null.
		 * 
		 * <p>For example: We have a class "Car" that has a property "driver" of class
		 * "Person", and class "Person" has a property "name". We pass into this
		 * function a car with no driver specified and the property "driver.name". If
		 * build is true, a new "Person" object is created and set as the driver of the car,
		 * and the name on that person is set. If build is false, no "Person" is added and
		 * the name is not set. If the car did have the driver property set, the build
		 * parameter would not make any difference.
		 */
		public static function setNestedProperty(object:Object, property:String, value:Object, build:Boolean=false):void {
			setChainedProperty(object, getChain(property), value, build);
		}
		
		/**
		 * Copy all the properties from src to dest. If dest is null, create a new instance
		 * of the same type as src before copying. Return the altered or new dest object.
		 */
		public static function shallowCopy(src:*, dest:*=null):Object {
			if (src is Array) {
				return copyArray(src as Array, dest as Array);
			}
			
			if (dest == null) {
				dest = new (MetaDataUtils.getType(src))();
			}
			
			//copy defined properties
			var xml:XML = describeType(src);
			var list:XMLList = xml.accessor;
			for(var i:int = 0; i < list.length(); i++) {
				if (list[i].@access == "readwrite") {
					var property:String = list[i].@name;
					dest[property] = src[property];
				}
			}
			list = xml.variable.@name;
			for(i = 0; i < list.length(); i++) {
				property = list[i];
				dest[property] = src[property];
			}
			//copy dynamic properties
			for(var p:* in src) {
				dest[p] = src[p];
			}
			return dest;
		}
		
		/**
		 * Replace the elements of the dest array with those of the src array
		 * so that the arrays are essentially the same. If dest is null, create
		 * a new array and copy the elements of src into it. Return the
		 * altered or new dest array.
		 */
		public static function copyArray(src:Array, dest:Array=null):Array {
			if (dest == null) {
				return (src != null && src.length > 0) ? src.concat() : [];
			} else {
				for (var i:int = dest.length; i >= 0; i--) {
					dest.pop();
				}
				if (src != null) {
					for each(var o:* in src) {
						dest.push(o);
					}
				}
				return dest;
			}
		}
		
		/**
		 * Return true if the arrays are the same length and every element in
		 * one is equal to its equivalent in the other. Otherwise return false.
		 * If itemEqualsFunction is specified, it is used to determine equality
		 * of individual elements. If not, == is used.
		 */
		public static function arraysEqual(a:Array, b:Array, itemEqualsFunction:Function=null):Boolean {
			if (a == null && b == null) return true;
			if (a == null || b == null) return false;
			if (a.length != b.length) return false;
			for (var i:int = 0; i < a.length; i++) {
				if (itemEqualsFunction != null)
					if (!itemEqualsFunction(a[i], b[i])) return false
				else
					if (a[i] != b[i]) return false;
			}
			return true;
		}
		
		/**
		 * Search a Container’s descendants for the first one for which the
		 * foundFunction returns true. If depthFirst is true, search depth
		 * first; otherwise search breadth first.
		 */
		public static function descendantSearch(parent:Container, foundFunction:Function, depthFirst:Boolean=true):DisplayObject {
			if (parent == null) return null;
			if (depthFirst) {
				for each (var child:DisplayObject in parent.getChildren()) {
					if (foundFunction(child)) {
						return child;
					} else if (child is Container) {
						var temp:DisplayObject = descendantSearch(child as Container, foundFunction, depthFirst);
						if (temp != null) return temp;
					}
				}
				return null;
			} else {
				throw Error("Breadth First Search not yet implemented");
			}
		}
		
		/**
		 * Search a collection or array for the first index (starting at startAt)
		 * of an item that matches object. If compareField is null, == is used
		 * for comparison. Otherwise, the objects are compared by the field
		 * specified. (Nested fields are allowed.)
		 */
		public static function indexOf(collection:*, object:*, compareField:String=null, startAt:int=0):int {
			if(compareField == null || compareField.length == 0) {
				//if no compare property is specified do a normal comparison on the whole object
				var i:int = startAt;
				if (collection is IViewCursor) {
					var cursor:IViewCursor = collection as IViewCursor;
					cursor.seek(CursorBookmark.FIRST, i);
					while (!cursor.afterLast) {
						if (cursor.current == object) return i;
						cursor.moveNext(); i++;
					}
				} else {
					for (; i < collection.length; i++) {
						if (collection[i] == object) return i;
					}
				}
				return -1; //not found
			} else {
				//if the compare property is specified compare on that property
				var value:* = getNestedProperty(object, compareField);
				return indexOfFieldValue(collection, value, compareField, startAt);
			}
		}
		
		public static function removeItem(collection:*, object:*, compareField:String=null, startAt:int=0):int {
			var i:int = indexOf(collection, object, compareField, startAt);
			if (i != -1)
				removeAt(collection, i);
			return i;
		}
		
		/**
		 * Search a collection or array for the first index (starting at startAt)
		 * of an item that matches object. === is used for comparison.
		 */
		public static function indexOfStrict(collection:*, object:*, startAt:int = 0):int {
				var i:int = startAt;
				if (collection is IViewCursor) {
					var cursor:IViewCursor = collection as IViewCursor;
					cursor.seek(CursorBookmark.FIRST, i);
					while (!cursor.afterLast) {
						if (cursor.current === object) return i;
						cursor.moveNext(); i++;
					}
				} else {
					for (; i < collection.length; i++) {
						if (collection[i] === object) return i;
					}
				}
				return -1; //not found			
		}
		
		public static function removeItemStrict(collection:*, object:*, startAt:int=0):int {
			var i:int = indexOfStrict(collection, object, startAt);
			if (i != -1)
				removeAt(collection, i);
			return i;
		}
			
		/**
		 * Search a collection or array for the first index (starting at startAt)
		 * of an item that has the given field equal to the given value. (Nested fields are allowed.)
		 */
		public static function indexOfFieldValue(collection:*, value:*, field:String, startAt:int=0):int {
			if (field == null || field.length == 0) return -1;
			var chain:Array = getChain(field);
			
			var i:int = startAt;
			if (collection is IViewCursor) {
				var cursor:IViewCursor = collection as IViewCursor;
				cursor.seek(CursorBookmark.FIRST, i);
				while (!cursor.afterLast) {
					if (getChainedProperty(cursor.current, chain) == value) return i;
					cursor.moveNext(); i++;
				}
			} else {
				for (; i < collection.length; i++) {
					if (getChainedProperty(collection[i], chain) == value) return i;
				}
			}
			return -1; //not found
		}
		
		public static function removeItemFieldValue(collection:*, value:*, field:String, startAt:int=0):int {
			var i:int = indexOfFieldValue(collection, value, field, startAt);
			if (i != -1)
				removeAt(collection, i);
			return i;
		}
				
		public static function removeAt(collection:*, index:int):void {
			if (collection is Array) {
				collection.splice(index, 1);
			} else if (collection is IList) {
				IList(collection).removeItemAt(index);
			} else if (collection is ICollectionView) {
				var cursor:IViewCursor = ICollectionView(collection).createCursor();
				cursor.seek(CursorBookmark.FIRST, index);
				cursor.remove();
			} else if (collection is IViewCursor) {
				IViewCursor(collection).seek(CursorBookmark.FIRST, index);
				IViewCursor(collection).remove();
			} else {
				throw new Error("Collection type '" + collection.constructor + "' not allowed in removeAt() method.");
			}	
		}
		
		/**
		 * Displays an Alert box. When the box is closed, the closeHandler
		 * function is called, and the result and data are passed into
		 * that function. The result is the code of the button that was
		 * pressed to close the box. The data is whatever data was passed
		 * into the data parameter of this confirm method, if any.
		 */
		public static function confirm(text:String=null, title:String=null, flags:uint=4, parent:Sprite=null, closeHandler:Function=null, data:*=null, iconClass:Class=null, defaultButtonFlag:uint=4):void {
			var alertBox:Alert = Alert.show(text, title, flags, parent, closeHandler != null ? confirmCallback : null, iconClass, defaultButtonFlag);
			alertBox.data = {callback: closeHandler, data: data};
		}
		
		private static function confirmCallback(event:CloseEvent):void {
			try {
				event.target.data.callback(event.detail, event.target.data.data);
			} catch (err:Error) {
				if (err.name == "ArgumentError") {
					event.target.data.callback(event.detail);
				}
			}
		}
		
		/**
		 * Returns true if x is a collection that the framework knows how
		 * to process (i.e. an Array, an ICollectionView or an IList).
		 */
		public static function isCollection(x:*):Boolean {
			return (x is Array || x is ICollectionView || x is IList);
		}
		
		/**
		 * Returns an Array representation of whatever collection (including an Array)
		 * that is passed in. WARNING: The data in the array and the collection may or
		 * not be bound and updated when future changes are made to either.
		 */
		public static function collectionToArray(collection:*):Array {
			if (collection is Array) {
				return collection as Array;
			} else if (collection is ArrayCollection) {
				return collection.source;
			} else if (collection is IList) {
				return IList(collection).toArray();
			} else if (collection is ICollectionView) {
				var cursor:IViewCursor = ICollectionView(collection).createCursor();
				var array:Array = [];
				cursor.seek(CursorBookmark.FIRST, -1);
				while (cursor.moveNext()) {
					array.push(cursor.current);
				}
				return array;
			}
			return null;
		}
		
		/**
		 * Returns an collection of the desired type filled with the values from
		 * the Array. WARNING: The data in the array and the collection may or not
		 * be bound and updated when future changes are made to either.
		 */
		public static function arrayToCollection(array:Array, collectionType:Class=null):* {
			if (collectionType == Array) {
				return array;
			} else if (collectionType == null || collectionType == ArrayCollection || collectionType == IList || collectionType == ICollectionView) {
				return new ArrayCollection(array);
			} else if (MetaDataUtils.typeIs(collectionType, IList)) {
				var list:IList = new collectionType();
				if (array != null && array.length > 0) {
					for each (var x:* in array) {
						list.addItem(x);
					}
				}
				return list;
			} else if (MetaDataUtils.typeIs(collectionType, ICollectionView)) {
				var collection:ICollectionView = new collectionType();
				if (array != null && array.length > 0) {
					var cursor:IViewCursor = collection.createCursor();
					for each (var y:* in array) {
						cursor.insert(y);
						cursor.moveNext();
					}
				}				
				return collection;
			}
			return null;
		}
		
		/**
		 * Calls a function with the maximum number of args from the args list that it will take.
		 * For example if a function takes 2 parameters and 3 args are passed into callFunc(),
		 * the target function will be called with the first two args.
		 * 
		 * The thisArg can be used to specify the object that 'this' refers to inside the function.
		 * It can be null.
		 */
		public static function callFunc(func:Function, thisArg:*, ...args:*):void {
			for (var i:int = args.length; i >= 0; i--) {
				try {
					func.apply(thisArg, args.slice(0, i));
					break;
				} catch (err:Error) {
					if (i > 0 && err.name == "ArgumentError" && err.message.substr(0, 12) == "Error #1063:") {
						continue;
					} else {
						throw err;
					}
				}
			}
		}
		
		public static function replaceProperties(string:String, ctx:*) {
			if (!string)
				return string;
				replacePropertiesCtx = ctx;
			return string.replace(/{([^}])}/, replacePropertiesReplace);
		}
		
		private static var replacePropertiesCtx:*;
		private static function replacePropertiesReplace(wholeMatch:String, property:String):String {
			return getNestedProperty(replacePropertiesCtx, property) as String;
		}
		
	}
}